/*!
 * \file WtRdmDtReaderAD.cpp
 * \project WonderTrader
 *
 * \author Wesley
 * \date 2020/03/30
 * 
 * \brief WonderTrader高级随机数据读取器实现文件
 * 
 * \details 该文件实现了WtRdmDtReaderAD类的完整功能，提供基于LMDB的高性能随机数据读取服务：
 *          
 *          核心功能模块：
 *          - LMDB数据库管理：分层管理K线和Tick数据库，优化存储结构
 *          - 智能缓存系统：内存缓存热点数据，支持增量更新和范围扩展
 *          - 时间范围查询：精确的时间窗口数据检索，支持跨交易日查询
 *          - 数量限制查询：按指定数量读取最新数据，适用于实时分析
 *          - 数据切片创建：高效的数据切片对象创建和管理
 *          
 *          性能优化特性：
 *          - 延迟加载：按需加载数据，减少内存占用
 *          - 增量缓存：只缓存新增数据，避免重复读取
 *          - 智能索引：使用二分查找快速定位数据
 *          - 内存复用：高效的内存分配和数据拷贝策略
 *          
 *          数据库组织：
 *          - K线数据：{basedir}/{period}/{exchg}/ (如：./min1/CFFEX/)
 *          - Tick数据：{basedir}/ticks/{exchg}/{code}/ (如：./ticks/CFFEX/IF2101/)
 */

#include "WtRdmDtReaderAD.h"
#include "LMDBKeys.h"

#include "../Includes/WTSVariant.hpp"
#include "../Share/TimeUtils.hpp"
#include "../Share/CodeHelper.hpp"
#include "../Share/StdUtils.hpp"

#include "../Includes/WTSContractInfo.hpp"
#include "../Includes/WTSSessionInfo.hpp"
#include "../Includes/IBaseDataMgr.h"
#include "../Includes/IHotMgr.h"
#include "../Includes/WTSDataDef.hpp"

//By Wesley @ 2022.01.05
#include "../Share/fmtlib.h"

/*!
 * \brief 管道式线程安全日志记录函数模板
 * \tparam Args 可变参数类型
 * \param sink 日志输出接口
 * \param ll 日志级别
 * \param format 格式化字符串
 * \param args 可变参数列表
 * 
 * \details 该函数提供线程安全的格式化日志输出功能：
 *          - 使用thread_local确保线程安全
 *          - 基于fmtlib提供高性能格式化
 *          - 支持任意类型参数的格式化输出
 *          - 自动管理内存缓冲区，避免内存泄漏
 */
template<typename... Args>
inline void pipe_rdmreader_log(IRdmDtReaderSink* sink, WTSLogLevel ll, const char* format, const Args&... args)
{
	if (sink == NULL)
		return;

	static thread_local char buffer[512] = { 0 };
	memset(buffer, 0, 512);
	fmt::format_to(buffer, format, args...);

	sink->reader_log(ll, buffer);
}

/*!
 * \brief C接口导出函数
 * 
 * \details 提供标准C接口，便于动态库加载和插件化使用
 */
extern "C"
{
	/*!
	 * \brief 创建随机数据读取器实例
	 * \return IRdmDtReader接口指针
	 */
	EXPORT_FLAG IRdmDtReader* createRdmDtReader()
	{
		IRdmDtReader* ret = new WtRdmDtReaderAD();
		return ret;
	}

	/*!
	 * \brief 删除随机数据读取器实例
	 * \param reader 要删除的读取器指针
	 */
	EXPORT_FLAG void deleteRdmDtReader(IRdmDtReader* reader)
	{
		if (reader != NULL)
			delete reader;
	}
};

/*!
 * \brief 构造函数
 * 
 * \details 初始化成员变量，设置默认值
 */
WtRdmDtReaderAD::WtRdmDtReaderAD()
	: _base_data_mgr(NULL)
	, _hot_mgr(NULL)
{
}

/*!
 * \brief 析构函数
 * 
 * \details 清理资源，LMDB数据库连接将自动关闭
 */
WtRdmDtReaderAD::~WtRdmDtReaderAD()
{
}

/*!
 * \brief 初始化随机数据读取器
 * \param cfg 配置参数
 * \param sink 回调接口
 * 
 * \details 该方法完成读取器的完整初始化：
 *          - 调用基类初始化方法
 *          - 获取基础数据管理器和热点管理器
 *          - 设置数据库根目录路径
 *          - 标准化路径格式，确保跨平台兼容性
 */
void WtRdmDtReaderAD::init(WTSVariant* cfg, IRdmDtReaderSink* sink)
{
	IRdmDtReader::init(cfg, sink);

	_base_data_mgr = sink->get_basedata_mgr();
	_hot_mgr = sink->get_hot_mgr();

	if (cfg == NULL)
		return;

	_base_dir = cfg->getCString("path");
	_base_dir = StrUtil::standardisePath(_base_dir);

	pipe_rdmreader_log(sink, LL_INFO, "WtRdmDtReaderAD initialized, root data folder is {}", _base_dir);
}

/*!
 * \brief 读取Tick数据切片（按数量）
 * \param stdCode 标准合约代码
 * \param count 数据数量
 * \param etime 结束时间（0表示到最新）
 * \return Tick数据切片
 * 
 * \note 当前版本暂未实现此功能，将在后续版本中提供
 */
WTSTickSlice* WtRdmDtReaderAD::readTickSlicesByCount(const char* stdCode, uint32_t count, uint64_t etime /* = 0 */)
{
	// TODO: 后续需要实现按数量读取的功能
	return NULL;
}

/*!
 * \brief 读取Tick数据切片（按时间范围）
 * \param stdCode 标准合约代码
 * \param stime 开始时间
 * \param etime 结束时间（0表示到最新）
 * \return Tick数据切片
 * 
 * \details 该方法实现高性能的Tick数据范围查询：
 *          
 *          查询流程：
 *          1. 解析合约代码，获取交易所和品种信息
 *          2. 获取交易时段信息，进行时间转换
 *          3. 计算交易日范围，处理跨日情况
 *          4. 检查缓存状态，判断是否需要从数据库读取
 *          5. 执行LMDB范围查询，获取原始数据
 *          6. 更新缓存，合并新旧数据
 *          7. 创建数据切片，返回结果
 *          
 *          缓存策略：
 *          - 空缓存：直接查询指定范围的所有数据
 *          - 需要更早数据：查询并前置插入到缓存
 *          - 需要更新数据：查询并后置追加到缓存
 *          - 缓存命中：直接从缓存中切片返回
 */
WTSTickSlice* WtRdmDtReaderAD::readTickSlicesByRange(const char* stdCode, uint64_t stime, uint64_t etime /* = 0 */)
{
	CodeHelper::CodeInfo cInfo = CodeHelper::extractStdCode(stdCode);
	WTSCommodityInfo* commInfo = _base_data_mgr->getCommodity(cInfo._exchg, cInfo._product);
	WTSSessionInfo* sInfo = commInfo->getSessionInfo();
	std::string stdPID = StrUtil::printf("%s.%s", cInfo._exchg, cInfo._product);

	uint32_t rDate, rTime, rSecs;
	// 时间格式：20190807124533900
	rDate = (uint32_t)(etime / 1000000000);
	rTime = sInfo->offsetTime((uint32_t)(etime % 1000000000) / 100000, false);
	rSecs = (uint32_t)(etime % 100000);

	uint32_t lDate, lTime, lSecs;
	// 时间格式：20190807124533900
	lDate = (uint32_t)(stime / 1000000000);
	lTime = sInfo->offsetTime((uint32_t)(stime % 1000000000) / 100000, true);
	lSecs = (uint32_t)(stime % 100000);

	uint32_t endTDate = _base_data_mgr->calcTradingDate(stdPID.c_str(), rDate, rTime, false);
	uint32_t beginTDate = _base_data_mgr->calcTradingDate(stdPID.c_str(), lDate, lTime, false);

	std::string key = stdCode;

	// 先检查缓存
	TicksList& tickList = _ticks_cache[key];

	bool isEmpty = tickList._ticks.empty();
	bool bNeedOlder = stime < tickList._first_tick_time;
	bool bNeedNewer = etime > tickList._last_tick_time;

	// 如果需要从LMDB中读取
	WtLMDBPtr db = get_t_db(cInfo._exchg, cInfo._code);
	if (db == NULL)
		return NULL;

	if (isEmpty)
	{
		// 缓存为空，读取所有数据
		WtLMDBQuery query(*db);
		LMDBHftKey lKey(cInfo._exchg, cInfo._code, beginTDate, lTime * 100000 + lSecs);
		LMDBHftKey rKey(cInfo._exchg, cInfo._code, endTDate, rTime * 100000 + rSecs);
		int cnt = query.get_range(std::string((const char*)&lKey, sizeof(lKey)),
			std::string((const char*)&rKey, sizeof(rKey)), [this, &tickList](const ValueArray& ayKeys, const ValueArray& ayVals) {
			tickList._ticks.resize(ayVals.size());
			std::size_t idx = 0;
			for (const std::string& item : ayVals)
			{
				memcpy(&tickList._ticks[idx], item.data(), item.size());
				idx++;
			}
		});

		if (cnt > 0)
		{
			WTSTickStruct& curTs = tickList._ticks.front();
			tickList._first_tick_time = (uint64_t)curTs.trading_date * 1000000000 + sInfo->offsetTime(curTs.action_time / 100000, true) + curTs.action_time % 100000;

			curTs = tickList._ticks.back();
			tickList._last_tick_time = (uint64_t)curTs.trading_date * 1000000000 + sInfo->offsetTime(curTs.action_time / 100000, false) + curTs.action_time % 100000;

			pipe_rdmreader_log(_sink, LL_DEBUG, "{} ticks between [{},{}] of {} loaded to cache", cnt, tickList._first_tick_time, tickList._last_tick_time, stdCode);
		}
	}
	else
	{
		if (bNeedOlder)
		{
			// 读取更早的数据
			WtLMDBQuery query(*db);
			LMDBHftKey rKey(cInfo._exchg, cInfo._code, (uint32_t)(tickList._first_tick_time / 1000000000), (uint32_t)(tickList._first_tick_time % 1000000000));
			LMDBHftKey lKey(cInfo._exchg, cInfo._code, beginTDate, lTime * 100000 + lSecs);
			int cnt = query.get_range(std::string((const char*)&lKey, sizeof(lKey)), std::string((const char*)&rKey, sizeof(rKey)),
				[this, &tickList](const ValueArray& ayKeys, const ValueArray& ayVals) {
				std::vector<WTSTickStruct> ayTicks;
				ayTicks.resize(ayVals.size() + tickList._ticks.size());
				std::size_t idx = 0;
				for (const std::string& item : ayVals)
				{
					WTSTickStruct* curTick = (WTSTickStruct*)item.data();
					memcpy(&ayTicks[idx], item.data(), item.size());
					idx++;
				}

				// 将原来的数据拷贝到后面，然后做一个swap操作
				memcpy(&ayTicks[idx], tickList._ticks.data(), sizeof(WTSTickStruct)*tickList._ticks.size());
				tickList._ticks.swap(ayTicks);
			});

			if(cnt > 0)
			{
				const WTSTickStruct& curTs = tickList._ticks.front();
				tickList._first_tick_time = (uint64_t)curTs.trading_date * 1000000000 + sInfo->offsetTime(curTs.action_time / 100000, false) + curTs.action_time % 100000;

				pipe_rdmreader_log(_sink, LL_DEBUG, "{} prev ticks of {} loaded to cache", cnt, stdCode);
			}
		}

		if (bNeedNewer)
		{
			// 读取更新的数据
			WtLMDBQuery query(*db);
			LMDBHftKey lKey(cInfo._exchg, cInfo._code, (uint32_t)(tickList._last_tick_time / 1000000000), (uint32_t)(tickList._last_tick_time % 1000000000));
			LMDBHftKey rKey(cInfo._exchg, cInfo._code, endTDate, rTime * 100000 + rSecs);
			int cnt = query.get_range(std::string((const char*)&lKey, sizeof(lKey)), std::string((const char*)&rKey, sizeof(rKey)),
				[this, &tickList](const ValueArray& ayKeys, const ValueArray& ayVals) {
				for (const std::string& item : ayVals)
				{
					WTSTickStruct* curTick = (WTSTickStruct*)item.data();
					tickList._ticks.emplace_back(*curTick);
				}
			});

			if (cnt > 0)
			{
				const WTSTickStruct& curTs = tickList._ticks.back();
				tickList._last_tick_time = (uint64_t)curTs.trading_date * 1000000000 + sInfo->offsetTime(curTs.action_time / 100000, true) + curTs.action_time % 100000;

				pipe_rdmreader_log(_sink, LL_DEBUG, "{} newer ticks of {} loaded to cache", cnt, stdCode);
			}
		}
	}

	// 全量获取完成后，创建数据切片
	{
		// 比较时间的对象
		WTSTickStruct sTick, eTick;
		sTick.action_date = lDate;
		sTick.action_time = (uint32_t)(stime % 1000000000);
		eTick.action_date = rDate;
		eTick.action_time = (uint32_t)(etime % 1000000000);

		std::size_t cnt = 0;

		WTSTickStruct* pTick = std::lower_bound(&tickList._ticks[0], &tickList._ticks[0] + (tickList._ticks.size() - 1), eTick, [](const WTSTickStruct& a, const WTSTickStruct& b) {
			if (a.action_date != b.action_date)
				return a.action_date < b.action_date;
			else
				return a.action_time < b.action_time;
		});

		std::size_t eIdx = pTick - &tickList._ticks[0];
		if (pTick->action_date > eTick.action_date || pTick->action_time >= eTick.action_time)
		{
			pTick--;
			eIdx--;
		}

		pTick = &tickList._ticks[0];
		// 如果第一个实时K线的时间大于开始时间，则实时K线要全部包含进去
		if (pTick->action_date > sTick.action_date || (pTick->action_date == sTick.action_date && pTick->action_time > sTick.action_time))
		{
			cnt = eIdx + 1;
		}
		else
		{
			pTick = std::lower_bound(&tickList._ticks[0], &tickList._ticks[0] + (tickList._ticks.size() - 1), sTick, [](const WTSTickStruct& a, const WTSTickStruct& b) {
				if (a.action_date != b.action_date)
					return a.action_date < b.action_date;
				else
					return a.action_time < b.action_time;
			});

			std::size_t sIdx = pTick - &tickList._ticks[0];
			cnt = eIdx - sIdx + 1;
		}

		WTSTickSlice* slice = WTSTickSlice::create(stdCode, pTick, cnt);
		return slice;
	}
}

/*!
 * \brief 读取K线数据切片（按数量）
 * \param stdCode 标准合约代码
 * \param period K线周期
 * \param count 数据数量
 * \param etime 结束时间（0表示到最新）
 * \return K线数据切片
 * 
 * \note 当前版本暂未实现此功能，将在后续版本中提供
 */
WTSKlineSlice* WtRdmDtReaderAD::readKlineSliceByCount(const char* stdCode, WTSKlinePeriod period, uint32_t count, uint64_t etime /* = 0 */)
{
	// TODO: 后续需要实现按数量读取的功能
	return NULL;
}

/*!
 * \brief 读取K线数据切片（按时间范围）
 * \param stdCode 标准合约代码
 * \param period K线周期
 * \param stime 开始时间
 * \param etime 结束时间（0表示到最新）
 * \return K线数据切片
 * 
 * \details 该方法实现高性能的K线数据范围查询：
 *          
 *          查询流程：
 *          1. 解析合约代码，获取交易所和品种信息
 *          2. 时间格式转换和交易日计算
 *          3. 检查缓存状态，判断是否需要更新
 *          4. 执行LMDB范围查询，获取K线数据
 *          5. 使用二分查找精确定位时间范围
 *          6. 创建K线数据切片并返回
 *          
 *          优化特性：
 *          - 支持日线和分钟线的不同时间处理逻辑
 *          - 智能缓存更新，避免重复读取
 *          - 高效的二分查找算法
 *          - 内存优化的数据结构
 */
WTSKlineSlice* WtRdmDtReaderAD::readKlineSliceByRange(const char* stdCode, WTSKlinePeriod period, uint64_t stime, uint64_t etime /* = 0 */)
{
	CodeHelper::CodeInfo cInfo = CodeHelper::extractStdCode(stdCode);
	WTSCommodityInfo* commInfo = _base_data_mgr->getCommodity(cInfo._exchg, cInfo._product);
	std::string stdPID = StrUtil::printf("%s.%s", cInfo._exchg, cInfo._product);

	uint32_t rDate, rTime, lDate, lTime;
	rDate = (uint32_t)(etime / 10000);
	rTime = (uint32_t)(etime % 10000);
	lDate = (uint32_t)(stime / 10000);
	lTime = (uint32_t)(stime % 10000);

	uint32_t endTDate = _base_data_mgr->calcTradingDate(stdPID.c_str(), rDate, rTime, false);
	uint32_t beginTDate = _base_data_mgr->calcTradingDate(stdPID.c_str(), lDate, lTime, false);

	bool isDay = period == KP_DAY;
	// 转换成交K线时间
	etime = isDay ? endTDate : (etime - 19000000);

	// 暂时数据和HOT之类模块，只有针对7个24小时品种做一定实现
	std::string key = StrUtil::printf("%s#%u", stdCode, period);
	BarsList& barsList = _bars_cache[key];

	bool bNeedNewer = (etime > barsList._last_bar_time);

	// 全量读取
	WtLMDBPtr db = get_k_db(cInfo._exchg, period);
	if (db == NULL)
		return NULL;

	if (barsList._bars.empty())
	{
		pipe_rdmreader_log(_sink, LL_DEBUG, "Reading back {} bars of {}.{}...", PERIOD_NAME[period], cInfo._exchg, cInfo._code);
		WtLMDBQuery query(*db);
		LMDBBarKey rKey(cInfo._exchg, cInfo._code, 0xffffffff);
		LMDBBarKey lKey(cInfo._exchg, cInfo._code, 0);
		int cnt = query.get_range(std::string((const char*)&lKey, sizeof(lKey)), std::string((const char*)&rKey, sizeof(rKey)),
			[this, &barsList, &lKey](const ValueArray& ayKeys, const ValueArray& ayVals) {
			if (ayVals.empty())
				return;

			std::size_t cnt = ayVals.size();
			barsList._bars.resize(cnt);
			std::size_t idx = 0;
			for (const std::string& item : ayVals)
			{
				memcpy(&barsList._bars[idx], item.data(), item.size());
				idx++;
			}
		});
	}
	else if(bNeedNewer)
	{
		// 读取更新的数据
		pipe_rdmreader_log(_sink, LL_DEBUG, "Reading back {} bars of {}.{}...", PERIOD_NAME[period], cInfo._exchg, cInfo._code);
		WtLMDBQuery query(*db);
		LMDBBarKey rKey(cInfo._exchg, cInfo._code, 0xffffffff);
		LMDBBarKey lKey(cInfo._exchg, cInfo._code, (uint32_t)barsList._last_bar_time);
		int cnt = query.get_range(std::string((const char*)&lKey, sizeof(lKey)), std::string((const char*)&rKey, sizeof(rKey)),
			[this, &barsList, &lKey](const ValueArray& ayKeys, const ValueArray& ayVals) {
			if (ayVals.empty())
				return;

			std::size_t cnt = ayVals.size();
			barsList._bars.resize(cnt);
			std::size_t idx = 0;
			for (const std::string& item : ayVals)
			{
				memcpy(&barsList._bars[idx], item.data(), item.size());
				idx++;
			}
		});
	}

	// 使用二分查找定位数据范围
	{
		WTSBarStruct eBar;
		eBar.date = rDate;
		eBar.time = (rDate - 19900000) * 10000 + rTime;

		WTSBarStruct sBar;
		sBar.date = lDate;
		sBar.time = (lDate - 19900000) * 10000 + lTime;

		WTSBarStruct* pHead = NULL;
		std::size_t cnt = 0;

		WTSBarStruct* pBar = std::lower_bound(&barsList._bars[0], &barsList._bars[0] + (barsList._bars.size() - 1), eBar, [isDay](const WTSBarStruct& a, const WTSBarStruct& b) {
			if (isDay)
				return a.date < b.date;
			else
				return a.time < b.time;
		});

		std::size_t idx = pBar - &barsList._bars[0];
		if ((isDay && pBar->date > eBar.date) || (!isDay && pBar->time > eBar.time))
		{
			pBar--;
			idx--;
		}

		pBar = &barsList._bars[0];
		// 如果第一个实时K线的时间大于开始时间，则实时K线要全部包含进去
		if ((isDay && pBar->date > sBar.date) || (!isDay && pBar->time > sBar.time))
		{
			pHead = pBar;
			cnt = idx + 1;
		}
		else
		{
			pBar = std::lower_bound(&barsList._bars[0], &barsList._bars[0] + (barsList._bars.size() - 1), sBar, [isDay](const WTSBarStruct& a, const WTSBarStruct& b) {
				if (isDay)
					return a.date < b.date;
				else
					return a.time < b.time;
			});

			std::size_t sIdx = pBar - &barsList._bars[0];
			pHead = pBar;
			cnt = idx - sIdx + 1;
		}

		WTSKlineSlice* slice = WTSKlineSlice::create(stdCode, period, 1, pHead, cnt);
		return slice;
	}
}

/*!
 * \brief 获取K线数据库实例
 * \param exchg 交易所代码
 * \param period K线周期
 * \return LMDB数据库智能指针
 * 
 * \details 该方法管理K线数据库连接池：
 *          
 *          数据库组织：
 *          - 1分钟K线：{basedir}/min1/{exchg}/
 *          - 5分钟K线：{basedir}/min5/{exchg}/
 *          - 日线数据：{basedir}/day/{exchg}/
 *          
 *          连接管理：
 *          - 按交易所和周期维护独立的数据库连接
 *          - 使用智能指针自动管理资源生命周期
 *          - 支持延迟初始化，按需创建数据库连接
 *          - 自动创建目录结构，确保数据库正常开启
 */
WtRdmDtReaderAD::WtLMDBPtr WtRdmDtReaderAD::get_k_db(const char* exchg, WTSKlinePeriod period)
{
	WtLMDBMap* the_map = NULL;
	std::string subdir;
	if (period == KP_Minute1)
	{
		the_map = &_exchg_m1_dbs;
		subdir = "min1";
	}
	else if (period == KP_Minute5)
	{
		the_map = &_exchg_m5_dbs;
		subdir = "min5";
	}
	else if (period == KP_DAY)
	{
		the_map = &_exchg_d1_dbs;
		subdir = "day";
	}
	else
		return std::move(WtLMDBPtr());

	auto it = the_map->find(exchg);
	if (it != the_map->end())
		return std::move(it->second);

	WtLMDBPtr dbPtr(new WtLMDB(true));
	std::string path = StrUtil::printf("%s%s/%s/", _base_dir.c_str(), subdir.c_str(), exchg);
	boost::filesystem::create_directories(path);
	if (!dbPtr->open(path.c_str()))
	{
		pipe_rdmreader_log(_sink, LL_ERROR, "Opening {} db if {} failed: {}", subdir, exchg, dbPtr->errmsg());
		return std::move(WtLMDBPtr());
	}
	else
	{
		pipe_rdmreader_log(_sink, LL_DEBUG, "{} db of {} opened", subdir, exchg);
	}

	(*the_map)[exchg] = dbPtr;
	return std::move(dbPtr);
}

/*!
 * \brief 获取Tick数据库实例
 * \param exchg 交易所代码
 * \param code 合约代码
 * \return LMDB数据库智能指针
 * 
 * \details 该方法管理Tick数据库连接池：
 *          
 *          数据库组织：
 *          - Tick数据：{basedir}/ticks/{exchg}/{code}/
 *          
 *          连接管理：
 *          - 按"交易所.合约"格式作为缓存键值
 *          - 使用智能指针自动管理资源生命周期
 *          - 支持延迟初始化，按需创建数据库连接
 *          - 自动创建目录结构，确保数据库正常开启
 * 
 * \note 修复了原有bug：使用完整的key而不是exchg作为缓存键值
 */
WtRdmDtReaderAD::WtLMDBPtr WtRdmDtReaderAD::get_t_db(const char* exchg, const char* code)
{
	std::string key = StrUtil::printf("%s.%s", exchg, code);
	auto it = _tick_dbs.find(key);
	if (it != _tick_dbs.end())
		return std::move(it->second);

	WtLMDBPtr dbPtr(new WtLMDB(true));
	std::string path = StrUtil::printf("%sticks/%s/%s", _base_dir.c_str(), exchg, code);
	boost::filesystem::create_directories(path);
	if (!dbPtr->open(path.c_str()))
	{
		pipe_rdmreader_log(_sink, LL_ERROR, "Opening tick db of {}.{} failed: {}", exchg, code, dbPtr->errmsg());
		return std::move(WtLMDBPtr());
	}
	else
	{
		pipe_rdmreader_log(_sink, LL_DEBUG, "Tick db of {}.{} opened", exchg, code);
	}

	// 修复bug：使用完整的key而不是exchg作为缓存键值
	_tick_dbs[key] = dbPtr;
	return std::move(dbPtr);
}